#include "exceptions.h"

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <sstream>
#include <string>
#include <algorithm>

#ifdef CA_HAVE_GLIBC_STACKTRACE
#include <execinfo.h>
#endif

#ifdef CA_HAVE_LIBSTDCXX_DEMANGLER
#include <cxxabi.h>
#endif

#include <vector>

namespace PS
{

namespace internals
{
std::vector<std::string>
break_text_into_lines(const std::string & original_text,
                      const unsigned int width,
                      const char delimiter)
{
    std::string text = original_text;
    std::vector<std::string> lines;

    // remove trailing spaces
    while ((text.size() != 0) && (text.back() == delimiter))
        text.erase(text.size() - 1, 1);

    // then split the text into lines
    while (text.size() != 0)
    {
        // in each iteration, first remove
        // leading spaces
        while ((text.size() != 0) && (text[0] == delimiter))
            text.erase(0, 1);

        std::size_t pos_newline = text.find_first_of('\n', 0);
        if (pos_newline != std::string::npos && pos_newline <= width)
        {
            std::string line(text, 0, pos_newline);
            while ((line.size() != 0) && (line.back() == delimiter))
                line.erase(line.size() - 1, 1);
            lines.push_back(line);
            text.erase(0, pos_newline + 1);
            continue;
        }

        // if we can fit everything into one
        // line, then do so. otherwise, we have
        // to keep breaking
        if (text.size() < width)
        {
            // remove trailing spaces
            while ((text.size() != 0) && (text.back() == delimiter))
                text.erase(text.size() - 1, 1);
            lines.push_back(text);
            text = "";
        }
        else
        {
            // starting at position width, find the
            // location of the previous space, so
            // that we can break around there
            int location = std::min<int>(width, text.size() - 1);
            for (; location > 0; --location)
                if (text[location] == delimiter)
                    break;

            // if there are no spaces, then try if
            // there are spaces coming up
            if (location == 0)
                for (location = std::min<int>(width, text.size() - 1);
                     location < static_cast<int>(text.size());
                     ++location)
                    if (text[location] == delimiter)
                        break;

            // now take the text up to the found
            // location and put it into a single
            // line, and remove it from 'text'
            std::string line(text, 0, location);
            while ((line.size() != 0) && (line.back() == delimiter))
                line.erase(line.size() - 1, 1);
            lines.push_back(line);
            text.erase(0, location);
        }
    }

    return lines;
}

std::string &
get_additional_assert_output()
{
    static std::string additional_assert_output;
    return additional_assert_output;
}

bool show_stacktrace = true;

bool allow_abort_on_exception = false;

} // namespace internals

void
set_additional_assert_output(const std::string & p)
{
    internals::get_additional_assert_output() = p;
}

void
suppress_stacktrace_in_exceptions()
{
    internals::show_stacktrace = false;
}

void
disable_abort_on_exception()
{
    internals::allow_abort_on_exception = false;
}

void
enable_abort_on_exception()
{
    internals::allow_abort_on_exception = true;
}

ExceptionBase::ExceptionBase()
    : file(""), line(0), function(""), cond(""), exc(""), n_stacktrace_frames(0), what_str("")
{
#ifdef CA_HAVE_GLIBC_STACKTRACE
    std::fill(std::begin(raw_stacktrace), std::end(raw_stacktrace), nullptr);
#endif
}

ExceptionBase::ExceptionBase(const ExceptionBase & exc)
    : file(exc.file),
      line(exc.line),
      function(exc.function),
      cond(exc.cond),
      exc(exc.exc),
      n_stacktrace_frames(exc.n_stacktrace_frames),
      what_str("") // don't copy the error message, it gets generated dynamically
                   // by what()
{
#ifdef CA_HAVE_GLIBC_STACKTRACE
    // Copy the raw_stacktrace pointers. We don't own them, they just point to the
    // addresses of symbols in the executable's/library's symbol tables -- and as
    // a consequence, it is safe to copy these pointers
    std::copy(
        std::begin(exc.raw_stacktrace), std::end(exc.raw_stacktrace), std::begin(raw_stacktrace));
#endif
}

void
ExceptionBase::set_fields(
    const char * f, const int l, const char * func, const char * c, const char * e)
{
    file = f;
    line = l;
    function = func;
    cond = c;
    exc = e;

    // If the system supports this, get a stacktrace how we got here:
    // Note that we defer the symbol lookup done by backtrace_symbols()
    // to when we need it (see what() below). This is for performance
    // reasons, as this requires loading libraries and can take in the
    // order of seconds on some machines.
#ifdef CA_HAVE_GLIBC_STACKTRACE
    n_stacktrace_frames = backtrace(raw_stacktrace, 25);
#endif
}

const char *
ExceptionBase::what() const noexcept
{
    // If no error c_string was generated so far, do it now:
    if (what_str.empty())
        generate_message();

    return what_str.c_str();
}

const char *
ExceptionBase::get_exc_name() const
{
    return exc;
}

void
ExceptionBase::print_exc_data(std::ostream & out) const
{
    // print a header for the exception
    out << "An error occurred at " << file << ":" << line << " in function" << std::endl
        << "    " << function << std::endl
        << "The violated condition was: " << std::endl
        << "    " << cond << std::endl;

    // print the way the additional information message was generated.
    // this is useful if the names of local variables appear in the
    // generation of the error message, because it allows the identification
    // of parts of the error text with what variables may have cause this
    //
    // On the other hand, this is almost never the case for ExcMessage
    // exceptions which would simply print the same text twice: once for
    // the way the message was composed, and once for the additional
    // information. Furthermore, the former of these two is often spread
    // between numerous "..."-enclosed strings that the preprocessor
    // collates into a single string, making it awkward to read. Consequently,
    // elide this text if the message was generated via an ExcMessage object
    if (std::strstr(cond, "PS::ExcMessage") != nullptr)
        out << "The name and call sequence of the exception was:" << std::endl
            << "    " << exc << std::endl;

    // finally print the additional information the exception provides:
    out << "Additional information: " << std::endl;
}

void
ExceptionBase::print_info(std::ostream & out) const
{
    out << "    (none)" << std::endl;
}

void
ExceptionBase::print_stack_trace(std::ostream & out) const
{
    if (n_stacktrace_frames == 0)
        return;

    if (internals::show_stacktrace == false)
        return;

    char ** stacktrace = nullptr;
#ifdef CA_HAVE_GLIBC_STACKTRACE
    // We have deferred the symbol lookup to this point to avoid costly
    // runtime penalties due to linkage of external libraries by
    // backtrace_symbols.
    stacktrace = backtrace_symbols(raw_stacktrace, n_stacktrace_frames);
#endif

    // if there is a stackframe stored, print it
    out << std::endl;
    out << "Stacktrace:" << std::endl << "-----------" << std::endl;

    // print the stacktrace. first omit all those frames that have
    // ExceptionBase or Exceptions in their names, as these
    // correspond to the exception raising mechanism themselves, rather than
    // the place where the exception was triggered
    int frame = 0;
    while ((frame < n_stacktrace_frames) &&
           ((std::string(stacktrace[frame]).find("ExceptionBase") != std::string::npos) ||
            (std::string(stacktrace[frame]).find("Exceptions") != std::string::npos)))
        ++frame;

    // output the rest
    const unsigned int first_significant_frame = frame;
    for (; frame < n_stacktrace_frames; ++frame)
    {
        out << '#' << frame - first_significant_frame << "  ";

        // the stacktrace frame is actually of the format
        // "filename(functionname+offset) [address]". let's try to get the
        // mangled functionname out:
        std::string stacktrace_entry(stacktrace[frame]);
        const unsigned int pos_start = stacktrace_entry.find('('),
                           pos_end = stacktrace_entry.find('+');
        std::string functionname = stacktrace_entry.substr(pos_start + 1, pos_end - pos_start - 1);

        stacktrace_entry = stacktrace_entry.substr(0, pos_start);
        stacktrace_entry += ": ";

        // demangle, and if successful replace old mangled string by
        // unmangled one (skipping address and offset). treat "main"
        // differently, since it is apparently demangled as "unsigned int"
        // for unknown reasons :-) if we can, demangle the function name
#ifdef CA_HAVE_LIBSTDCXX_DEMANGLER
        int status;
        char * p = abi::__cxa_demangle(functionname.c_str(), nullptr, nullptr, &status);

        if ((status == 0) && (functionname != "main"))
        {
            std::string realname(p);
            // in MT mode, one often gets backtraces spanning several lines
            // because we have so many boost::tuple arguments in the MT
            // calling functions. most of the trailing arguments of these
            // tuples are actually unused boost::tuples::null_type, so we
            // should split them off if they are trailing a template argument
            // list
            while (realname.find(", boost::tuples::null_type>") != std::string::npos)
                realname.erase(realname.find(", boost::tuples::null_type>"),
                               std::string(", boost::tuples::null_type").size());

            stacktrace_entry += realname;
        }
        else
            stacktrace_entry += functionname;

        free(p);

#else

        stacktrace_entry += functionname;
#endif

        // then output what we have
        out << stacktrace_entry << std::endl;

        // stop if we're in main()
        if (functionname == "main")
            break;
    }

    free(stacktrace); // free(nullptr) is allowed
    stacktrace = nullptr;
}

void
ExceptionBase::generate_message() const
{
    // build up a c_string with the error message.
    // Guard this procedure with a try block, we shall not throw at this
    // place...
    try
    {
        std::ostringstream converter;

        converter << std::endl
                  << "--------------------------------------------------------" << std::endl;

        // Print out general data
        print_exc_data(converter);

        // Print out exception specific data. Put this into another stringstream
        // object for now so that we can break long lines and print them in a
        // more easily read way
        {
            std::ostringstream message;
            print_info(message);

            const auto message_in_lines =
                internals::break_text_into_lines(message.str(), 80, ' ');

            // Put the message into the stream that will be output.
            for (const auto & line : message_in_lines)
                converter << "    " << line << '\n';
        }

        print_stack_trace(converter);

        if (!internals::get_additional_assert_output().empty())
        {
            converter << "--------------------------------------------------------" << std::endl
                      << internals::get_additional_assert_output() << std::endl;
        }

        converter << "--------------------------------------------------------" << std::endl;

        what_str = converter.str();
    }
    catch (...)
    {
        // On error, resume next. There is nothing better we can do...
        what_str = "ExceptionBase::generate_message () failed";
    }
}

namespace StandardExceptions
{

#ifdef CA_WITH_MPI
ExcMPI::ExcMPI(const int error_code) : error_code(error_code) {}

void
ExcMPI::print_info(std::ostream & out) const
{
    char error_name[MPI_MAX_ERROR_STRING];
    error_name[0] = '\0';
    int resulting_length = MPI_MAX_ERROR_STRING;

    bool error_name_known = false;
    // workaround for Open MPI 1.6.5 not accepting
    // MPI_ERR_LASTCODE in MPI_Error_class
    if (error_code < MPI_ERR_LASTCODE)
    {
        // get the string name of the error code by first converting it to an
        // error class.
        int error_class = 0;
        int ierr = MPI_Error_class(error_code, &error_class);
        error_name_known = (ierr == MPI_SUCCESS);

        // Check the output of the error printing functions. If either MPI
        // function fails we should just print a less descriptive message.
        if (error_name_known)
        {
            ierr = MPI_Error_string(error_class, error_name, &resulting_length);
            error_name_known = error_name_known && (ierr == MPI_SUCCESS);
        }
    }

    out << "yhpde encountered an error while calling an MPI function." << std::endl;
    if (error_name_known)
    {
        out << "The description of the error provided by MPI is \"" << error_name << "\"."
            << std::endl;
    }
    else
    {
        out << "This error code is not equal to any of the standard MPI error codes." << std::endl;
    }
    out << "The numerical value of the original error code is " << error_code << '.' << std::endl;
}
#endif // CA_WITH_MPI

} // namespace StandardExceptions

namespace internals
{

[[noreturn]] void
abort(const ExceptionBase & exc) noexcept
{
    // first print the error
    std::cerr << exc.what() << std::endl;

    // then bail out. if in MPI mode, bring down the entire
    // house by asking the MPI system to do a best-effort
    // operation at also terminating all of the other MPI
    // processes. this is useful because if only one process
    // runs into an assertion, then that may lead to deadlocks
    // if the others don't recognize this, or at the very least
    // delay their termination until they realize that their
    // communication with the job that died times out.
    //
    // Unlike std::abort(), MPI_Abort() unfortunately doesn't break when
    // running inside a debugger like GDB, so only use this strategy if
    // absolutely necessary and inform the user how to use a debugger.
#ifdef CA_WITH_MPI
    int is_initialized;
    MPI_Initialized(&is_initialized);
    if (is_initialized != 0)
    {
        // do the same as in Utilities::MPI::n_mpi_processes() here,
        // but without error checking to not throw again.
        const int n_proc = MPI::Adapter::size();
        if (n_proc > 1)
        {
            std::cerr << "Calling MPI_Abort now.\n"
                      << "To break execution in a GDB session, execute 'break MPI_Abort' "
                         "before "
                      << "running. You can also put the following into your ~/.gdbinit:\n"
                      << "  set breakpoint pending on\n"
                      << "  break MPI_Abort\n"
                      << "  set breakpoint pending auto" << std::endl;

            MPI_Abort(MPI_COMM_WORLD,
                      /* return code = */ 255);
        }
    }
#endif
    std::abort();
}

void
do_issue_error_nothrow(const ExceptionBase & exc) noexcept
{
    if (internals::allow_abort_on_exception)
        abort(exc);
    else
    {
        // We are not allowed to throw, and not allowed to abort.
        // Just print the exception name to logger and continue normally:
        std::cout << "Exception: " << exc.get_exc_name() << std::endl;
        std::cout << exc.what() << std::endl;
    }
}
} // namespace internals

} // namespace PS
